linux下TCP socket编程入门案例(二)

您所在的位置:网站首页 tcp 阻塞 非阻塞 linux下TCP socket编程入门案例(二)

linux下TCP socket编程入门案例(二)

2023-08-14 08:52| 来源: 网络整理| 查看: 265

文章目录 1、相关概念介绍1.1 阻塞与非阻塞1.2 两者区别1.3 select模型 2、编码实现2.1 代码改进2.2 实现服务端客户端 3、运行结果 在 上一篇【阻塞的TCP server&client】中,介绍了如何使用socket函数编写第一个socket通信小程序。这篇文章在第一个demo的基础上,将使用select函数实现非阻塞的TCP server&client。

1、相关概念介绍 1.1 阻塞与非阻塞

在理解这个概念前,你要知道在linux系统中,一切皆是文件,不管是普通文件、输入输出设备、目录,或者是套接字,都被linux当做文件处理。

1)阻塞是指,当试图对某个文件描述符进行读写时,如果当前没有东西可读,或者暂时不可写,程序就进入等待状态,直到有东西可读或者可写为止。 而对于非阻塞状态,如果没有东西可读,或者不可写,读写函数马上返回,而不会等待(这也与设置的超时时间有关)。 2)非阻塞,就是进程或线程执行某个函数时不必非要等待事件的发生,一旦执行肯定返回,以返回值的不同来反映函数的执行情况,如果事件发生则与阻塞方式相同,若事件没有发生则返回一个代码来告知事件未发生,而进程或线程继续执行,所以效率较高。

1.2 两者区别

①阻塞好控制,不发送完数据程序不会往下走,但是对性能有影响; 非阻塞不太好控制,可能和能力有关,但是性能会得到很大提升。 ②阻塞式的编程方便,非阻塞的编程不方便,需要开发人员处理各种返回; ③阻塞处理简单,非阻塞处理复杂; ④阻塞效率低,非阻塞效率高; ⑤阻塞模式,常见的通信模型为多线程模型,服务端accept之后,对每个socket创建一个线程去recv。逻辑上简单,适用于并发量小(客户端数目少),连续传输大数据量的情况下,比如文件服务器。还有就是在客户端接收服务器消息的时候也经常用,因为客户端就一个socket,用阻塞模式不影响效率,而且编程逻辑上要简单得多。 非阻塞模式,常见的通信模型为select模型和IOCP模型,适用于高并发,数据量小的情况,比如聊天室;客户端多的情况下,如果采用阻塞模式,需要开很多线程,影响效率。

注意,不要和同步、异步的概念搞混了。下面的解释来源于网络:

I.所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回。 按照这个定义,其实绝大多数函数都是同步调用(例如sin, isdigit等)。但是一般而言,我们在说同步、异步的时候,特指那些需要其他部件协作或者需要一定时间完成的任务。最常见的例子就是 SendMessage。该函数发送一个消息给某个窗口,在对方处理完消息之前,这个函数不返回。当对方处理完毕以后,该函数才把消息处理函数所返回的 LRESULT值返回给调用者。 II.异步的概念和同步相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。以 CAsycSocket类为例(注意,CSocket从CAsyncSocket派生,但是其功能已经由异步转化为同步),当一个客户端通过调用 Connect函数发出一个连接请求后,调用者线程立刻可以向下运行。当连接真正建立起来以后,socket底层会发送一个消息通知该对象。这里提到执行部件和调用者通过三种途径返回结果:状态、通知和回调。可以使用哪一种依赖于执行部件的实现,除非执行部件提供多种选择,否则不受调用者控制。如果执行部件用状态来通知,那么调用者就需要每隔一定时间检查一次,效率就很低(有些初学多线程编程的人,总喜欢用一个循环去检查某个变量的值,这其实是一种很严重的错误)。如果是使用通知的方式,效率则很高,因为执行部件几乎不需要做额外的操作。至于回调函数,其实和通知没太多区别。

同步/异步和阻塞/非阻塞的区别

1.阻塞调用是指调用结果返回之前,当前线程会被挂起。函数只有在得到结果之后才会返回; 对同步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回而已 2.非阻塞是指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。 1.3 select模型

好了,对这些有了了解后,可以帮助你更好的理解select模型。 函数原型: int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset,struct timeval *timeout); 返回值:成功返回做好准备的文件描述符的个数;超时为0;错误为 -1. 参数: maxfdp:是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1,不能错。+1的原因:[0,maxfd],描述符是从0开始的,因此如果最大的描述符为n的话,共有n+1个描述符。 fd_set *readset:指向fd_set结构的指针,监视这些文件描述符的读变化; fd_set *writeset:监视集合中文件描述符的写变化,只要有一个文件可写,函数就返回一个大于0的值; fd_set *exceptset:同上,监视集合中错误异常文件; struct timeval *timeout:select的超时时间。这个参数至关重要,它可以使select处于三种状态, 第一,若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,一直等到监视文件描述符集合中某个文件描述符发生变化为止; 第二,若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值; 第三,timeout的值大于0,这就是等待的超时时间,即 select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,返回值同上述。

I.传递给select函数的参数会告诉内核:

我们所关心的文件描述符对每个描述符,我们所关心的状态。(是想从一个文件描述符中读或者写,还是关注一个描述符中是否出现异常)要等待多长时间。(可以等待无限长的时间;等待固定的一段时间;或者根本就不等待)

II.从select函数返回后,内核会告诉我们:

对我们的要求已经做好准备的描述符的个数对于三种条件(读,写,异常)哪些描述符已经做好准备

对fd_set类型的变量,可以使用以下几个宏控制它 int FD_ZERO(int fd, fd_set *set); //将一个 fd_set类型变量的所有位都置为 0

int FD_CLR(int fd, fd_set *set); //清除某个位

int FD_SET(int fd, fd_set *set); //将变量的某个位置位

int FD_ISSET(int fd, fd_set *set); //测试某个位是否被置位

理解select模型,关键是理解fd_set。为了方便说明,以fd_set长度为1B(1字节)为例,fd_set的每一位(bit)可以对应一个文件描述符(fd)。则1B的fd_set可以对应8个fd.

1)执行fd_set set,FD_ZERO(&set);则set用位表示是0000,0000 2)若fd=3,执行FD_SET(fd,&set);后set变为0000,0100(第3位为1) 3)若再加入fd=2,fd=1,则set变为0000,0111 4)执行select(5,&set,0,0,0)阻塞等待 5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=3位置被清空。

注意, a.可监控的文件描述符个数取决与sizeof(fd_set)的值; b.将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd,一是用于在select返回后,把array作为源数据和fd_set进行FD_ISSET判断。二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。 c.必须在select前循环array(加入fd,取maxfd),select返回后循环array(使用FD_ISSET判断是否有(读/写/异常)事件发生)。

2、编码实现 2.1 代码改进

在阻塞的TCP server&client中,加入select函数 即在listen之后,将套接字描述符全部加入fd_set,然后按照下面的顺序编写代码 1)FD_ZERO()清空fd_set; 2)FD_SET()将要测试的fd加入fd_set; 3)select()测试fd_set中所有的fd; 4)FD_ISSET()测试是否有符合条件的描述符

2.2 实现 服务端

server.cpp

/* * server.cpp --非阻塞TCP server * * Created on: Nov 23, 2019 * Author: xb */ #include #include #include #include #include #include #include #include #define CONCURRENT_MAX 3 //服务端同时支持的最大连接数 #define SERVER_PORT 9999 //端口 #define BUFFER_SIZE 1024 //缓冲区大小 int main(int argc, char* argv[]) { int client_fd[CONCURRENT_MAX] = {0};//用于存放客户端套接字描述符 int server_sock_fd;//服务器端套接字描述符 char send_msg[BUFFER_SIZE];//数据传输缓冲区 char recv_msg[BUFFER_SIZE]; struct sockaddr_in server_addr; memset(&server_addr,0,sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(SERVER_PORT); //server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");//限定只接受本地连接请求 server_addr.sin_addr.s_addr = INADDR_ANY; /*测试*/ /*for(int a = 0; a < CONCURRENT_MAX; a++){ printf("client_fd[%d] = %d\n",a,client_fd[a]); }*/ //1.创建socket server_sock_fd = socket(AF_INET, SOCK_STREAM, 0); if (server_sock_fd printf("bind socket error:%s(errno:%d)\n",strerror(errno),errno); return -1; } //3.监听listen if (listen(server_sock_fd, 5) tv.tv_sec = 10;/* 超时时间10s */ tv.tv_usec = 0; /* 1)FD_ZERO()清空fd_set; 2)FD_SET()将要测试的fd加入fd_set; 3)select()测试fd_set中所有的fd; 4)FD_ISSET()测试是否有符合条件的描述符 */ FD_ZERO(&server_fd_set); //STDIN_FILENO:接收键盘输入 FD_SET(STDIN_FILENO, &server_fd_set); if (max_fd max_fd = server_sock_fd; } //客户端连接 for (int i = 0; i FD_SET(client_fd[i], &server_fd_set); if (max_fd printf("select error:%s(errno:%d)\n",strerror(errno),errno); continue; } else if (result == 0) { printf("select 超时\n"); continue; } else { //result为位状态发生变化的文件描述符的个数 //STDIN_FILENO:系统API接口库,是打开文件的句柄 /* 服务器端输入信息 */ if (FD_ISSET(STDIN_FILENO, &server_fd_set)) { bzero(send_msg, BUFFER_SIZE);//清空 scanf("%s",&send_msg); //输入"q"则关闭服务端 if (strcmp(send_msg, "q") == 0) { close(server_sock_fd); exit(0); } for (int i = 0; i printf("发送消息给客户端:"); printf("client_fd[%d]=%d\n", i, client_fd[i]); send(client_fd[i], send_msg, strlen(send_msg), 0); } } } /* 处理新的连接请求 */ if (FD_ISSET(server_sock_fd, &server_fd_set)) { struct sockaddr_in client_address; socklen_t address_len; int client_sock_fd = accept(server_sock_fd,(struct sockaddr *) &client_address, &address_len); printf("new connection client_sock_fd = %d\n", client_sock_fd); if (client_sock_fd > 0) { int index = -1;//判断连接数量是否达到最大值 for (int i = 0; i index = i; client_fd[i] = client_sock_fd; break; } } if (index >= 0) { printf("新客户端[%d] [%s:%d]连接成功\n", index, inet_ntoa(client_address.sin_addr), ntohs(client_address.sin_port)); } else { bzero(send_msg, BUFFER_SIZE); strcpy(send_msg, "服务器已连接的客户端数量达到最大值,连接失败!\n"); send(client_sock_fd, send_msg, strlen(send_msg), 0); printf("客户端连接数量达到最大值,新客户端[%s:%d]连接失败\n", inet_ntoa(client_address.sin_addr), ntohs(client_address.sin_port)); } } } /* 处理某个客户端发过来的消息 */ for (int i = 0; i if (FD_ISSET(client_fd[i], &server_fd_set)) { bzero(recv_msg, BUFFER_SIZE); int n = recv(client_fd[i], recv_msg,BUFFER_SIZE, 0); // >0,接收消息成功 if (n > 0) { if (n > BUFFER_SIZE) { n = BUFFER_SIZE; } recv_msg[n] = '\0'; printf("收到客户端[%d]发来的消息:%s\n", i, recv_msg); } else if (n //=0,对端连接关闭 FD_CLR(client_fd[i], &server_fd_set); client_fd[i] = 0; printf("客户端[%d]断开连接\n", i); } } } } } } return 0; } 客户端

client.cpp

/* * client.cpp -- 非阻塞 TCP client * * Created on: Nov 23, 2019 * Author: xb */ #include #include #include #include #include #include #include #include #define BUFFER_SIZE 1024 int main(int argc, char* argv[]) { char recv_msg[BUFFER_SIZE]; char send_msg[BUFFER_SIZE];//数据收发缓冲区 struct sockaddr_in server_addr; int server_sock_fd; memset(&server_addr,0,sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(9999); server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //创建套接字 if ((server_sock_fd = socket(AF_INET, SOCK_STREAM, 0)) printf("connect error: %s(errno: %d)\n",strerror(errno),errno); return -1; } fd_set client_fd_set; struct timeval tv; while (1) { tv.tv_sec = 2; tv.tv_usec = 0; FD_ZERO(&client_fd_set); FD_SET(STDIN_FILENO, &client_fd_set); FD_SET(server_sock_fd, &client_fd_set); select(server_sock_fd + 1, &client_fd_set, NULL, NULL, &tv); if (FD_ISSET(STDIN_FILENO, &client_fd_set)) { bzero(send_msg, BUFFER_SIZE); fgets(send_msg, BUFFER_SIZE, stdin); if (send(server_sock_fd, send_msg, BUFFER_SIZE, 0) bzero(recv_msg, BUFFER_SIZE); int n = recv(server_sock_fd, recv_msg, BUFFER_SIZE, 0); if (n > 0) { printf("recv %d byte\n",n); if (n > BUFFER_SIZE) { n = BUFFER_SIZE; } recv_msg[n] = '\0'; printf("收到服务器发送的信息:%s\n", recv_msg); } else if (n printf("服务器已关闭!\n"); close(server_sock_fd); exit(0); } } } return 0; } 3、运行结果

使用g++编译: g++ server.cpp -o server g++ client.cpp -o client 程序运行结果: 1)启动server 在这里插入图片描述 2)启动client 在这里插入图片描述 3)发送信息给server 在这里插入图片描述 4)发送信息给client 在这里插入图片描述 第4个client尝试连接server时 在这里插入图片描述 相比较阻塞的TCP server&client,非阻塞的可以连接更多客户端。阻塞的则只能连接一个,新的连接请求会被阻塞,直到上一个连接关闭。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3